package com.googlecode.quillen.util;

import com.googlecode.quillen.domain.Chunk;

import java.io.InputStream;
import java.io.IOException;
import static java.lang.System.arraycopy;
import java.security.NoSuchAlgorithmException;

/**
 * Created by IntelliJ IDEA.
 * User: greg
 * Date: Dec 2, 2008
 * Time: 9:55:49 PM
 */

// TODO: maybe this oughta be in the service layer, instead of in the util package.
    
public class StreamChunker
{
    // TODO: put these values into properties file?
    
    private static final int BOUNDARY_SIZE = 48;
    private static final int BOUNDARY_MASK = 0x1FFFF;   // Low order 17 bits => expected average chunk size of 128K
    private static final int BOUNDARY_VALUE = 0x55555555 & BOUNDARY_MASK;
    private static final int DEFAULT_MIN_CHUNK_SIZE = 2 * 1000;
    private static final int DEFAULT_MAX_CHUNK_SIZE = 5 * 1000 * 1000;
    private static final int BUFFER_SIZE = 3 * DEFAULT_MAX_CHUNK_SIZE;

    private InputStream in;
    private byte[] buffer;
    private int bufferedSize;
    private int offset;
    private int minChunkSize;
    private int maxChunkSize;

    public StreamChunker(InputStream in)
    {
        this(in, DEFAULT_MIN_CHUNK_SIZE, DEFAULT_MAX_CHUNK_SIZE);
    }

    public StreamChunker(InputStream in, int minChunkSize, int maxChunkSize)
    {
        if (minChunkSize < BOUNDARY_SIZE)
        {
            throw new IllegalArgumentException("minChunkSize must be greater than " + BOUNDARY_SIZE);
        }

        if (maxChunkSize > BUFFER_SIZE)
        {
            throw new IllegalArgumentException("maxChunkSize must be less than " + BUFFER_SIZE);
        }

        this.in = in;
        this.minChunkSize = minChunkSize;
        this.maxChunkSize = maxChunkSize;
        
        buffer = new byte[BUFFER_SIZE];
        bufferedSize = 0;
        offset = 0;
    }

    public Chunk getNextChunk() throws IOException, NoSuchAlgorithmException
    {
        do
        {
            while (bufferedSize < maxChunkSize && in != null)
            {
                if (offset > 0)
                {
                    arraycopy(buffer, offset, buffer, 0, bufferedSize);
                    offset = 0;
                }

                int bytesRead = in.read(buffer, bufferedSize, buffer.length - bufferedSize);

                if (bytesRead < 0)
                {
                    in.close();
                    in = null;
                }
                else
                {
                    bufferedSize += bytesRead;
                }
            }

            if (bufferedSize >= minChunkSize)
            {
                for (int i = minChunkSize; i < bufferedSize - BOUNDARY_SIZE; i++)
                {
                    int fingerprint = RabinHashFunction.DEFAULT_HASH_FUNCTION.hash(buffer, offset + i, BOUNDARY_SIZE);

                    if ((fingerprint & BOUNDARY_MASK) == BOUNDARY_VALUE || i >= maxChunkSize)
                    {
                        Chunk chunk = new Chunk(buffer, offset, i);

                        bufferedSize -= i;
                        offset += i;

                        return chunk;
                    }
                }
            }

            if (bufferedSize > 0 && (in == null || bufferedSize >= maxChunkSize))
            {
                Chunk chunk = new Chunk(buffer, offset, bufferedSize);

                bufferedSize = 0;
                offset = 0;
                
                return chunk;
            }
        }
        while (bufferedSize > 0);

        return null;
    }
}